Deploying a Quarto Site to Azure Storage Accounts

Complete guide to deploying Quarto documentation to Azure Storage Account static websites with automated CI/CD
Author

Dario Airoldi

Published

January 15, 2025

Modified

November 3, 2025

This appendix provides a comprehensive guide to deploying your Quarto documentation site to Azure Storage Account Static Website hosting, including setup, configuration, CDN integration, and automated deployment workflows.

📋 Table of Contents

📖 Overview

Azure Storage Account Static Website hosting provides a cost-effective, scalable solution for hosting Quarto documentation sites.

It offers excellent performance, global distribution via Azure CDN, and seamless integration with Azure DevOps and GitHub Actions.

Key Benefits

  • 💰 Cost-effective: Pay only for storage and bandwidth used
  • 🚀 High performance: Built-in CDN integration
  • 🌍 Global distribution: Azure’s worldwide infrastructure
  • 🔒 Secure: HTTPS by default, custom domain support
  • 🔄 CI/CD integration: Works with Azure DevOps, GitHub Actions
  • 📈 Scalable: Handles traffic spikes automatically

✅ Prerequisites

Before deploying to Azure Storage, ensure you have:

  • Azure subscription with appropriate permissions
  • Azure CLI or Azure PowerShell installed
  • Quarto installed locally
  • Basic understanding of Azure services
  • Repository with your Quarto project (GitHub, Azure DevOps, etc.)

🏗️ Architecture Overview

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Source Code   │───▶│  Build Pipeline  │───▶│ Azure Storage   │
│ (GitHub/DevOps) │    │ (GitHub Actions/ │    │ Static Website  │
└─────────────────┘    │  Azure DevOps)   │    └─────────────────┘
                       └──────────────────┘              │
                                                         ▼
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│  Custom Domain  │◀───│   Azure CDN      │◀───│   $web Container│
│  (docs.site.com)│    │ (Optional but    │    │   (HTML files)  │
└─────────────────┘    │  Recommended)    │    └─────────────────┘
                       └──────────────────┘

⚙️ Setup Azure Infrastructure

Step 1: Create Storage Account

Using Azure CLI

# Set variables
RESOURCE_GROUP="docs-rg"
STORAGE_ACCOUNT="yourdocsstorage"  # Must be globally unique
LOCATION="East US"

# Create resource group
az group create --name $RESOURCE_GROUP --location "$LOCATION"

# Create storage account
az storage account create \
  --name $STORAGE_ACCOUNT \
  --resource-group $RESOURCE_GROUP \
  --location "$LOCATION" \
  --sku Standard_LRS \
  --kind StorageV2 \
  --access-tier Hot

# Enable static website hosting
az storage blob service-properties update \
  --account-name $STORAGE_ACCOUNT \
  --static-website \
  --index-document index.html \
  --404-document 404.html

Using Azure PowerShell

# Set variables
$ResourceGroupName = "docs-rg"
$StorageAccountName = "yourdocsstorage"  # Must be globally unique
$Location = "East US"

# Create resource group
New-AzResourceGroup -Name $ResourceGroupName -Location $Location

# Create storage account
$storageAccount = New-AzStorageAccount `
  -ResourceGroupName $ResourceGroupName `
  -Name $StorageAccountName `
  -Location $Location `
  -SkuName "Standard_LRS" `
  -Kind "StorageV2" `
  -AccessTier Hot

# Enable static website hosting
Enable-AzStorageStaticWebsite `
  -Context $storageAccount.Context `
  -IndexDocument "index.html" `
  -ErrorDocument404Path "404.html"

Step 2: Configure Quarto for Azure Deployment

Update your _quarto.yml for Azure hosting:

project:
  type: website
  output-dir: docs
  
website:
  title: "Your Documentation Site"
  site-url: "https://yourdocsstorage.z13.web.core.windows.net"  # Your storage URL
  description: "Technical documentation hosted on Azure"
  
  navbar:
    background: primary
    search: true
    left:
      - href: index.qmd
        text: Home
    right:
      - icon: github
        href: "https://github.com/username/repository"
        
format:
  html:
    theme: cosmo
    toc: true
    anchor-sections: true
    smooth-scroll: true
    code-copy: true
    html-math-method: katex
    link-external-newwindow: true
    # Optimize for CDN caching
    embed-resources: false
    minimal: false

🚀 Deployment Methods

Method 1: GitHub Actions Deployment

Create .github/workflows/deploy-azure-storage.yml:

name: Deploy Quarto Site to Azure Storage

on:
  push:
    branches: [main]
  workflow_dispatch:

env:
  AZURE_STORAGE_ACCOUNT: yourdocsstorage
  AZURE_STORAGE_CONTAINER: $web

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      
    - name: Setup Quarto
      uses: quarto-dev/quarto-actions/setup@v2
      with:
        version: 'release'
        
    - name: Install dependencies
      run: |
        # Add any additional dependencies
        # pip install -r requirements.txt
        # npm install
        
    - name: Render Quarto project
      run: quarto render
      
    - name: Login to Azure
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}
        
    - name: Upload to Azure Storage
      uses: azure/CLI@v1
      with:
        inlineScript: |
          # Remove existing files (optional - for clean deployment)
          az storage blob delete-batch \
            --account-name $AZURE_STORAGE_ACCOUNT \
            --source $AZURE_STORAGE_CONTAINER \
            --pattern "*"
            
          # Upload new files
          az storage blob upload-batch \
            --account-name $AZURE_STORAGE_ACCOUNT \
            --destination $AZURE_STORAGE_CONTAINER \
            --source ./docs \
            --overwrite true
            
          # Set content types for proper serving
          az storage blob upload-batch \
            --account-name $AZURE_STORAGE_ACCOUNT \
            --destination $AZURE_STORAGE_CONTAINER \
            --source ./docs \
            --pattern "*.html" \
            --content-type "text/html" \
            --overwrite true
            
          az storage blob upload-batch \
            --account-name $AZURE_STORAGE_ACCOUNT \
            --destination $AZURE_STORAGE_CONTAINER \
            --source ./docs \
            --pattern "*.css" \
            --content-type "text/css" \
            --overwrite true
            
          az storage blob upload-batch \
            --account-name $AZURE_STORAGE_ACCOUNT \
            --destination $AZURE_STORAGE_CONTAINER \
            --source ./docs \
            --pattern "*.js" \
            --content-type "text/javascript" \
            --overwrite true
            
    - name: Purge CDN Cache (if using CDN)
      uses: azure/CLI@v1
      with:
        inlineScript: |
          # Replace with your CDN profile and endpoint names
          az cdn endpoint purge \
            --resource-group ${{ env.RESOURCE_GROUP }} \
            --profile-name your-cdn-profile \
            --name your-endpoint \
            --content-paths "/*"
      continue-on-error: true

Setting up Azure Service Principal

  1. Create Service Principal:
az ad sp create-for-rbac \
  --name "QuartoDeployment" \
  --role contributor \
  --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
  --sdk-auth
  1. Add GitHub Secret:
    • Go to GitHub repository settings
    • Add secret named AZURE_CREDENTIALS
    • Paste the JSON output from the service principal creation

Method 2: Azure DevOps Pipeline

Create azure-pipelines.yml:

trigger:

- main

pool:
  vmImage: 'ubuntu-latest'

variables:
  storageAccountName: 'yourdocsstorage'
  containerName: '$web'

stages:

- stage: Build
  displayName: 'Build Quarto Site'
  jobs:
  - job: Build
    displayName: 'Build'
    steps:
    - task: UsePythonVersion@0
      inputs:
        versionSpec: '3.x'
        addToPath: true
        
    - script: |
        # Install Quarto
        curl -LO https://quarto.org/download/latest/quarto-linux-amd64.deb
        sudo dpkg -i quarto-linux-amd64.deb
      displayName: 'Install Quarto'
      
    - script: |
        quarto render
      displayName: 'Render Quarto Project'
      
    - task: PublishBuildArtifacts@1
      inputs:
        pathtoPublish: 'docs'
        artifactName: 'quarto-site'
        
- stage: Deploy
  displayName: 'Deploy to Azure Storage'
  dependsOn: Build
  condition: succeeded()
  jobs:
  - deployment: Deploy
    displayName: 'Deploy'
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: DownloadBuildArtifacts@0
            inputs:
              buildType: 'current'
              downloadType: 'single'
              artifactName: 'quarto-site'
              downloadPath: '$(System.ArtifactsDirectory)'
              
          - task: AzureCLI@2
            displayName: 'Deploy to Storage Account'
            inputs:
              azureSubscription: 'your-service-connection'
              scriptType: 'bash'
              scriptLocation: 'inlineScript'
              inlineScript: |
                # Upload files to storage account
                az storage blob upload-batch \
                  --account-name $(storageAccountName) \
                  --destination $(containerName) \
                  --source $(System.ArtifactsDirectory)/quarto-site \
                  --overwrite true
                  
                # Set proper content types
                az storage blob upload-batch \
                  --account-name $(storageAccountName) \
                  --destination $(containerName) \
                  --source $(System.ArtifactsDirectory)/quarto-site \
                  --pattern "*.html" \
                  --content-type "text/html" \
                  --overwrite true

Method 3: Manual Deployment with Azure CLI

For quick testing or one-off deployments:

# Render site locally
quarto render

# Upload to Azure Storage
az storage blob upload-batch \
  --account-name yourdocsstorage \
  --destination '$web' \
  --source ./docs \
  --overwrite true

# Set content types
az storage blob upload-batch \
  --account-name yourdocsstorage \
  --destination '$web' \
  --source ./docs \
  --pattern "*.html" \
  --content-type "text/html" \
  --overwrite true

az storage blob upload-batch \
  --account-name yourdocsstorage \
  --destination '$web' \
  --source ./docs \
  --pattern "*.css" \
  --content-type "text/css" \
  --overwrite true

🌐 Azure CDN Integration

Why Use Azure CDN?

  • Global Performance: Content cached at edge locations worldwide
  • Custom Domain Support: Use your own domain with SSL
  • Compression: Automatic gzip compression
  • Caching Control: Fine-grained cache control
  • DDoS Protection: Built-in protection against attacks

Setting up Azure CDN

# Create CDN profile
az cdn profile create \
  --name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP \
  --sku Standard_Microsoft

# Create CDN endpoint
az cdn endpoint create \
  --name "docs-endpoint" \
  --profile-name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP \
  --origin yourdocsstorage.z13.web.core.windows.net \
  --origin-host-header yourdocsstorage.z13.web.core.windows.net

# Configure caching rules
az cdn endpoint rule add \
  --name "docs-endpoint" \
  --profile-name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP \
  --order 1 \
  --rule-name "CacheHTML" \
  --match-variable RequestUri \
  --operator EndsWith \
  --match-values "*.html" \
  --action-name CacheExpiration \
  --cache-behavior Override \
  --cache-duration "1.00:00:00"  # 1 day

CDN Configuration for Quarto Sites

# Set up compression
az cdn endpoint update \
  --name "docs-endpoint" \
  --profile-name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP \
  --content-types-to-compress \
    "text/html" \
    "text/css" \
    "application/javascript" \
    "text/javascript" \
    "application/json" \
    "text/plain" \
  --is-compression-enabled true

# Configure HTTPS redirect
az cdn endpoint update \
  --name "docs-endpoint" \
  --profile-name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP \
  --https-redirect Enabled

🌍 Custom Domain Configuration

Step 1: Add Custom Domain to CDN

# Add custom domain to CDN endpoint
az cdn custom-domain create \
  --name "docs-domain" \
  --endpoint-name "docs-endpoint" \
  --profile-name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP \
  --hostname "docs.yoursite.com"

# Enable HTTPS on custom domain
az cdn custom-domain enable-https \
  --name "docs-domain" \
  --endpoint-name "docs-endpoint" \
  --profile-name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP

Step 2: DNS Configuration

Configure your DNS provider:

# CNAME record for subdomain
docs.yoursite.com. CNAME docs-endpoint.azureedge.net.

# Or for apex domain, use Azure DNS
@. ALIAS docs-endpoint.azureedge.net.

Step 3: Update Quarto Configuration

website:
  site-url: "https://docs.yoursite.com"  # Your custom domain

🔧 Advanced Configuration

Environment-Specific Deployments

Create different storage accounts for different environments:

# _quarto-dev.yml
website:
  site-url: "https://devdocsstorage.z13.web.core.windows.net"

# _quarto-prod.yml
website:
  site-url: "https://docs.yoursite.com"

Deploy with environment-specific configuration:

# Development
quarto render --profile dev

# Production
quarto render --profile prod

Security Headers

Add security headers using CDN rules:

# Add security headers via CDN rules
az cdn endpoint rule add \
  --name "docs-endpoint" \
  --profile-name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP \
  --order 2 \
  --rule-name "SecurityHeaders" \
  --action-name ModifyResponseHeader \
  --header-action Append \
  --header-name "X-Frame-Options" \
  --header-value "DENY"

az cdn endpoint rule add \
  --name "docs-endpoint" \
  --profile-name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP \
  --order 3 \
  --rule-name "ContentSecurityPolicy" \
  --action-name ModifyResponseHeader \
  --header-action Append \
  --header-name "Content-Security-Policy" \
  --header-value "default-src 'self'; script-src 'self' 'unsafe-inline';"

Analytics Integration

Enable Azure Application Insights for detailed analytics:

<!-- Add to _includes/analytics.html -->
<script type="text/javascript">
var appInsights=window.appInsights||function(a){
  function b(a){c[a]=function(){var b=arguments;c.queue.push(function(){c[a].apply(c,b)})}}var c={config:a},d=document,e=window;setTimeout(function(){var b=d.createElement("script");b.src=a.url||"https://az416426.vo.msecnd.net/scripts/a/ai.0.js",d.getElementsByTagName("script")[0].parentNode.appendChild(b)});try{c.cookie=d.cookie}catch(a){}c.queue=[];for(var f=["Event","Exception","Metric","PageView","Trace","Dependency"];f.length;)b("track"+f.pop());if(b("setAuthenticatedUserContext"),b("clearAuthenticatedUserContext"),b("startTrackEvent"),b("stopTrackEvent"),b("startTrackPage"),b("stopTrackPage"),b("flush"),!a.disableExceptionTracking){f="onerror",b("_"+f);var g=e[f];e[f]=function(a,b,d,e,h){var i=g&&g(a,b,d,e,h);return!0!==i&&c["_"+f](a,b,d,e,h),i}}return c
    }({
        instrumentationKey: "YOUR_INSTRUMENTATION_KEY"
    });

window.appInsights=appInsights,appInsights.queue&&0===appInsights.queue.length&&appInsights.trackPageView();
</script>

Include in Quarto configuration:

format:
  html:
    include-in-header:
      - _includes/analytics.html

📊 Monitoring and Optimization

Cost Monitoring

Set up cost alerts in Azure:

# Create budget for storage account
az consumption budget create \
  --budget-name "docs-site-budget" \
  --amount 50 \
  --resource-group $RESOURCE_GROUP \
  --time-grain Monthly \
  --start-date "2025-01-01T00:00:00Z" \
  --end-date "2025-12-31T00:00:00Z"

Performance Monitoring

Monitor your site performance:

  1. Azure Monitor: Set up alerts for storage account metrics
  2. Application Insights: Track user behavior and performance
  3. CDN Analytics: Monitor cache hit ratios and bandwidth usage

Backup and Disaster Recovery

# Enable soft delete for blobs
az storage account blob-service-properties update \
  --account-name $STORAGE_ACCOUNT \
  --enable-delete-retention true \
  --delete-retention-days 30

# Enable versioning
az storage account blob-service-properties update \
  --account-name $STORAGE_ACCOUNT \
  --enable-versioning true

🔍 Troubleshooting Common Issues

1. Files Not Serving Correctly

Problem: HTML files download instead of displaying.

Solution: Set correct content types during upload:

az storage blob upload-batch \
  --account-name $STORAGE_ACCOUNT \
  --destination '$web' \
  --source ./docs \
  --pattern "*.html" \
  --content-type "text/html"

2. CDN Not Updating

Problem: Changes don’t appear due to CDN caching.

Solution: Purge CDN cache after deployment:

az cdn endpoint purge \
  --resource-group $RESOURCE_GROUP \
  --profile-name "docs-cdn-profile" \
  --name "docs-endpoint" \
  --content-paths "/*"

3. Custom Domain SSL Issues

Problem: HTTPS not working on custom domain.

Solutions:

# Check certificate status
az cdn custom-domain show \
  --name "docs-domain" \
  --endpoint-name "docs-endpoint" \
  --profile-name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP

# Validate domain ownership
az cdn custom-domain list \
  --endpoint-name "docs-endpoint" \
  --profile-name "docs-cdn-profile" \
  --resource-group $RESOURCE_GROUP

💰 Cost Optimization

Storage Costs

  • Use Hot access tier for frequently accessed documentation
  • Enable lifecycle management for old versions
  • Monitor storage usage with Azure Cost Management

CDN Costs

  • Configure appropriate caching rules to maximize cache hit ratios
  • Use compression to reduce bandwidth costs
  • Consider geo-filtering if your audience is in specific regions

🔄 Migration from Other Platforms

From GitHub Pages

  1. Export/clone your repository
  2. Set up Azure Storage Account
  3. Update site-url in _quarto.yml
  4. Configure new deployment pipeline

From Netlify

  1. Download site files or use Git repository
  2. Update build commands for Azure deployment
  3. Migrate environment variables to Azure Key Vault
  4. Set up custom domain in Azure CDN

🎯 Conclusion

Azure Storage Account static website hosting provides a robust, scalable, and cost-effective solution for hosting Quarto documentation sites. Key advantages include:

  • Global Performance: CDN integration for worldwide content delivery
  • Enterprise Integration: Seamless integration with Azure DevOps and other Azure services
  • Cost Control: Pay-per-use pricing with predictable costs
  • Security: Built-in security features and custom domain SSL support
  • Scalability: Automatic scaling to handle traffic spikes

This solution is ideal for:

  • Enterprise documentation requiring integration with Azure services
  • High-traffic sites needing global CDN distribution
  • Multi-environment deployments (dev, staging, production)
  • Sites requiring advanced monitoring and analytics

📚 Additional Resources